﻿#region MyJScript - LGPL Copyright (c) 2008 Lionel Laské
//
// This file is part of MyJScript.
//
// MyJScript is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// MyJScript is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with MyJScript; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
//
//================================================================================
#endregion

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Scripting;
using Microsoft.Scripting.Ast;
using Microsoft.Scripting.Actions;
using Microsoft.Scripting.Generation;
using Microsoft.Scripting.Runtime;
using MyJScript.Runtime;

namespace MyJScript.DLR
{
    /// <summary>
    /// Binder for MyJScript: create rules, handle conversion.
    /// </summary>
    public class MJSBinder : ActionBinder
    {
        /// <summary>
        /// Constructor, call parent.
        /// </summary>
        /// <param name="context">context</param>
        public MJSBinder(CodeContext context) : base(context)
        {
        }

        /// <summary>
        /// Called when a new rule is need.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="callerContext">context</param>
        /// <param name="action">current</param>
        /// <param name="args">current args for action</param>
        /// <returns></returns>
        protected override StandardRule<T> MakeRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
        {
            // Rules for SetMember
            if (action.Kind == DynamicActionKind.SetMember)
            {
                // Rules exist only for MyJScript object
                if (!(args[0] is MJSObject))
                    return base.MakeRule<T>(callerContext, action, args);

                // Create rule
                return MakeSetMemberRule<T>(callerContext, action, args);
            }

            // Rules for GetMember
            else if (action.Kind == DynamicActionKind.GetMember)
            {
                // Rules for String object: catch "length" member
                GetMemberAction getmember = (GetMemberAction)action;
                if (args[0] is string && SymbolTable.IdToString(getmember.Name).Equals("length"))
                {
                    // Create rule
                    return MakeGetMemberStringRule<T>(callerContext, action, args);
                }

                // Rules for MyJScript object
                else if (!(args[0] is MJSObject))
                    return base.MakeRule<T>(callerContext, action, args);

                // Make rule
                return MakeGetMemberObjectRule<T>(callerContext, action, args);
            }

            // Rules for member
            else if (action.Kind == DynamicActionKind.InvokeMember)
            {
                // Rule exists only for MJSObject
                if (!(args[0] is MJSObject))
                    return base.MakeRule<T>(callerContext, action, args);

                // Make rule
                return MakeInvokeMemberRule<T>(callerContext, action, args);
            }

            // Rules for operations
            else if (action.Kind != DynamicActionKind.DoOperation)
                return base.MakeRule<T>(callerContext, action, args);
            DoOperationAction operation = (DoOperationAction)action;

            // Rules for ==
            if (operation.Operation == Operators.Equals)
            {
                // Rules exists only to compare one string to something else
                if (!(args[0] is string) && !(args[1] is string))
                    return base.MakeRule<T>(callerContext, action, args);
                if (args[0] is string && args[1] is string)
                    return base.MakeRule<T>(callerContext, action, args);

                // Create rule
                return MakeEqualsStringRule<T>(callerContext, action, args);
            }

            // Rules for >, <, >= and <=
            else if (operation.Operation == Operators.GreaterThan
                || operation.Operation == Operators.LessThan
                || operation.Operation == Operators.GreaterThanOrEqual
                || operation.Operation == Operators.LessThanOrEqual)
            {
                // Rules exists only to compare one string to an int
                if (!(args[0] is string) && !(args[1] is string))
                    return base.MakeRule<T>(callerContext, action, args);
                if (args[0] is string && args[1] is string)
                    return base.MakeRule<T>(callerContext, action, args);
                if (!(args[0] is int) && !(args[1] is int))
                    return base.MakeRule<T>(callerContext, action, args);

                // Create rule
                return MakeCompareStringRule<T>(callerContext, action, args, operation.Operation);
            }

            // Rules for +
            else if (operation.Operation == Operators.Add)
            {
                // Rules exists only to concat strings
                if (!(args[0] is string) && !(args[1] is string))
                    return base.MakeRule<T>(callerContext, action, args);

                // Create rule
                return MakeAddStringRule<T>(callerContext, action, args);
            }

            return base.MakeRule<T>(callerContext, action, args);

        }

        #region Rules detail
        /// <summary>
        /// Helper to create GetMember rule on String.
        /// </summary>
        /// <typeparam name="T">T</typeparam>
        /// <param name="action">action</param>
        /// <param name="args">args</param>
        /// <param name="callerContext">caller context</param>
        /// <returns>rule</returns>
        private StandardRule<T> MakeGetMemberStringRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
        {
            // Set test: if (arg0 is string)                  
            StandardRule<T> rule = new StandardRule<T>();
            rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(string));

            // Set target: use Length
            rule.Target = 
                rule.MakeReturn(this,
                    Ast.Action.GetMember(
                        SymbolTable.StringToId("Length"),
                        typeof(object),
                        rule.Parameters[0]
                    )
                )
            ;

            return rule;
        }

        /// <summary>
        /// Helper to create SetMember rule.
        /// </summary>
        /// <typeparam name="T">T</typeparam>
        /// <param name="action">action</param>
        /// <param name="args">args</param>
        /// <param name="callerContext">caller context</param>
        /// <returns>rule</returns>
        private StandardRule<T> MakeSetMemberRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
        {
            // Set test: if (arg0 is MJSObject)
            SetMemberAction setmember = (SetMemberAction)action;
            StandardRule<T> rule = new StandardRule<T>();
            rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(MJSObject));

            // Set target
            rule.Target =
                rule.MakeReturn(this,
                    Ast.Call(
                        Ast.Convert(rule.Parameters[0], typeof(MJSObject)),
                        typeof(MJSObject).GetMethod("Set", new Type[] { typeof(string), typeof(object) }),
                        Ast.Constant(SymbolTable.IdToString(setmember.Name)),
                        Ast.Convert(rule.Parameters[1], typeof(object))
                    )
                )
            ;

            return rule;
        }

        /// <summary>
        /// Helper to create GetMember rule on MJSObject.
        /// </summary>
        /// <typeparam name="T">T</typeparam>
        /// <param name="action">action</param>
        /// <param name="args">args</param>
        /// <param name="callerContext">caller context</param>
        /// <returns>rule</returns>
        private StandardRule<T> MakeGetMemberObjectRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
        {
            // Set test: if (arg0 is MJSObject)
            StandardRule<T> rule = new StandardRule<T>();
            GetMemberAction getmember = (GetMemberAction)action;
            rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(MJSObject));

            // Set target
            rule.Target = 
                rule.MakeReturn(this,
                    Ast.Call(
                        Ast.Convert(rule.Parameters[0], typeof(MJSObject)),
                        typeof(MJSObject).GetMethod("Get", new Type[] { typeof(string) }),
                        Ast.Constant(SymbolTable.IdToString(getmember.Name))
                    )
                )
            ;

            return rule;
        }

        /// <summary>
        /// Helper to create Add rule on string.
        /// </summary>
        /// <typeparam name="T">T</typeparam>
        /// <param name="action">action</param>
        /// <param name="args">args</param>
        /// <param name="callerContext">caller context</param>
        /// <returns>rule</returns>
        private StandardRule<T> MakeAddStringRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
        {
            // Build test and target for first parameter
            Expression tst0, tgt0;
            StandardRule<T> rule = new StandardRule<T>();
            if (args[0] is string)
            {
                tst0 = Ast.TypeIs(rule.Parameters[0], typeof(string));
                tgt0 = Ast.Convert(rule.Parameters[0], typeof(string));
            }
            else if (args[0] is int)
            {
                tst0 = Ast.TypeIs(rule.Parameters[0], typeof(int));
                tgt0 = Ast.Call(
                            Ast.Convert(rule.Parameters[0], typeof(object)),
                            typeof(object).GetMethod("ToString", new Type[0]));
            }
            else
                return base.MakeRule<T>(callerContext, action, args);

            // Build test and target for second parameter
            Expression tst1, tgt1;
            if (args[1] is string)
            {
                tst1 = Ast.TypeIs(rule.Parameters[1], typeof(string));
                tgt1 = Ast.Convert(rule.Parameters[1], typeof(string));
            }
            else if (args[1] is int)
            {
                tst1 = Ast.TypeIs(rule.Parameters[1], typeof(int));
                tgt1 = Ast.Call(
                            Ast.Convert(rule.Parameters[1], typeof(object)),
                            typeof(object).GetMethod("ToString", new Type[0]));
            }
            else
                return base.MakeRule<T>(callerContext, action, args);

            // Set test: tst0 && tst1
            rule.Test = Ast.AndAlso(tst0, tst1);

            // Set Rules: string.Concat(tgt0, tgt1);
            rule.Target =
                rule.MakeReturn(this,
                    Ast.Call(
                        typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }),
                        tgt0,
                        tgt1)
                )
            ;

            return rule;
        }

        /// <summary>
        /// Helper to create Equals rule on string.
        /// </summary>
        /// <typeparam name="T">T</typeparam>
        /// <param name="action">action</param>
        /// <param name="args">args</param>
        /// <param name="callerContext">caller context</param>
        /// <returns>rule</returns>
        private StandardRule<T> MakeEqualsStringRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
        {
            // Build test and target for first parameter
            Expression tst0, tgt0;
            StandardRule<T> rule = new StandardRule<T>();
            if (args[0] is string)
            {
                tst0 = Ast.TypeIs(rule.Parameters[0], typeof(string));
                tgt0 = Ast.Convert(rule.Parameters[0], typeof(string));
            }
            else if (args[0] is int)
            {
                tst0 = Ast.TypeIs(rule.Parameters[0], typeof(int));
                tgt0 = Ast.Call(
                            Ast.Convert(rule.Parameters[0], typeof(object)),
                            typeof(object).GetMethod("ToString", new Type[0]));
            }
            else
                return base.MakeRule<T>(callerContext, action, args);

            // Build test and target for second parameter
            Expression tst1, tgt1;
            if (args[1] is string)
            {
                tst1 = Ast.TypeIs(rule.Parameters[1], typeof(string));
                tgt1 = Ast.Convert(rule.Parameters[1], typeof(string));
            }
            else if (args[1] is int)
            {
                tst1 = Ast.TypeIs(rule.Parameters[1], typeof(int));
                tgt1 = Ast.Call(
                            Ast.Convert(rule.Parameters[1], typeof(object)),
                            typeof(object).GetMethod("ToString", new Type[0]));
            }
            else
                return base.MakeRule<T>(callerContext, action, args);

            // Set test: tst0 && tst1
            rule.Test = Ast.AndAlso(tst0, tst1);

            // Set Rules: tgt0.Equals(tgt1);
            rule.Target =
                rule.MakeReturn(this,
                    Ast.Call(
                        tgt0,
                        typeof(string).GetMethod("Equals", new Type[] { typeof(string) }),
                        tgt1)
                )
            ;

            return rule;
        }

        /// <summary>
        /// Helper to create compare rule on string.
        /// </summary>
        /// <typeparam name="T">T</typeparam>
        /// <param name="action">action</param>
        /// <param name="args">args</param>
        /// <param name="callerContext">caller context</param>
        /// <returns>rule</returns>
        private StandardRule<T> MakeCompareStringRule<T>(CodeContext callerContext, DynamicAction action, object[] args, Operators operation)
        {
            // Build test 
            Expression tst0, tst1;
            StandardRule<T> rule = new StandardRule<T>();
            Expression r0 = rule.Parameters[0];
            Expression r1 = rule.Parameters[1];
            if (args[0] is string)
            {
                tst0 = Ast.TypeIs(rule.Parameters[0], typeof(string));
                tst1 = Ast.TypeIs(rule.Parameters[1], typeof(int));
                r1 = Ast.Convert(r1, typeof(object));
            }
            else
            {
                tst0 = Ast.TypeIs(rule.Parameters[0], typeof(int));
                r0 = Ast.Convert(r0, typeof(object));
                tst1 = Ast.TypeIs(rule.Parameters[1], typeof(string));
            }
            rule.Test = Ast.AndAlso(tst0, tst1);

            // Build target: MJSLibrary.CompareTo(arg0, arg1) > 0
            Expression tgtcompare;
            tgtcompare = Ast.Call(
                typeof(MJSLibrary).GetMethod("CompareTo"),
                r0,
                r1
            );
            Expression tgtvalue = null;
            if (operation == Operators.GreaterThan)
                tgtvalue = Ast.GreaterThan(tgtcompare, Ast.Constant(0));
            else if (operation == Operators.LessThan)
                tgtvalue = Ast.LessThan(tgtcompare, Ast.Constant(0));
            else if (operation == Operators.GreaterThanOrEqual)
                tgtvalue = Ast.GreaterThanEquals(tgtcompare, Ast.Constant(0));
            else if (operation == Operators.LessThanOrEqual)
                tgtvalue = Ast.LessThanEquals(tgtcompare, Ast.Constant(0));
            rule.Target = rule.MakeReturn(this, tgtvalue);

            return rule;
        }

        /// <summary>
        /// Helper to create invoke a member on MJSObject.
        /// Need to add instance parameter.
        /// </summary>
        /// <typeparam name="T">T</typeparam>
        /// <param name="action">action</param>
        /// <param name="args">args</param>
        /// <param name="callerContext">caller context</param>
        /// <returns>rule</returns>
        private StandardRule<T> MakeInvokeMemberRule<T>(CodeContext callerContext, DynamicAction action, object[] args)
        {
            // Set test: if (arg0 is MJSFunction)
            StandardRule<T> rule = new StandardRule<T>();
            InvokeMemberAction invokeMember = (InvokeMemberAction)action;
            rule.Test = Ast.TypeIs(rule.Parameters[0], typeof(MJSObject));

            // Get method to call
            Expression method = Ast.Action.GetMember(
                invokeMember.Name,
                typeof(object),
                rule.Parameters[0]
             );

            // Create a new array
            Expression[] newparam = new Expression[rule.Parameters.Count + 1];
            newparam[0] = method;
            for (int i = 0; i < rule.Parameters.Count; i++)
                newparam[i + 1] = rule.Parameters[i];

            // Set target
            rule.Target = 
                rule.MakeReturn(this,
                    Ast.Action.Call(
                        typeof(object),
                        newparam
                    )
                )
            ;

            return rule;
        }

        #endregion

        #region ActionBinder overrides

        public override bool CanConvertFrom(Type fromType, Type toType, NarrowingLevel level)
        {
            return toType.IsAssignableFrom(fromType);
        }

        public override bool PreferConvert(Type t1, Type t2)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}
